package com.itheima.mp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.extension.toolkit.Db;
import com.itheima.mp.domain.po.Address;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.domain.vo.AddressVO;
import com.itheima.mp.domain.vo.UserVO;
import com.itheima.mp.enums.UserStatus;
import com.itheima.mp.mapper.UserMapper;
import com.itheima.mp.service.IUserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;
import java.util.stream.Collectors;


/**
 * MP的Service接口使用流程是怎样的？
 *
 * 1.自定义Service接口继承IService接口
 * 2.自定义Service实现类，实现自定义接口并继承ServiceImpl类
 *
 * 由于Service中经常需要定义与业务有关的自定义方法，因此我们不能直接使用IService，而是自定义Service接口，然后继承IService以拓展方法。同时，让自定义的Service实现类继承ServiceImpl，这样就不用自己实现IService中的接口了。
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {


    /**
     *
     *     总结：在什么情况下，怎么使用MP中提供的Service和Mapper中的方法？
     *     对于一些简单的方法，可以直接在controller里调用MP提供的Service方法，无需任何自定义Service或Mapper。
     *     当业务逻辑相对复杂，需要自己写一些业务，而MP只提供基本的增删改查，没有业务，此时需要自定义Service方法，并且在其中编写业务逻辑。
     *     当BaseMapper或Service提供的方法不足以满足增删改查的需求时，需要编写mapper，自定义SQL语句。
     *
     * @param id
     * @param money
     */
    @Override
    @Transactional
    public void deductBalance(Long id, Integer money) {


        // 1.查询用户(可以用ServiceImpl中的getById，或用BaseMapper中的查询方法)
        User user = getById(id);

        // 2.校验用户状态
        if(user == null || user.getStatus() == UserStatus.FROZEN){

            throw new RuntimeException("用户状态异常！");
        }

        // 3.校验余额是否充足
        if (user.getBalance() < money){

            throw new RuntimeException("用户余额不足！");
        }

        // 4.扣减余额 update tb_user set balance = balance - ?
        //baseMapper.dedectBalance(id,money);
        // 不建议用MP实现，因为需要在业务层写SQL语句，故建议在自定义的SQL语句写，即在mapper层写。

        /*IService中还提供了Lambda功能来简化我们的复杂查询及更新功能。
        Service中对LambdaQueryWrapper和LambdaUpdateWrapper的用法进一步做了简化。
        我们无需自己通过new的方式来创建Wrapper，而是直接调用lambdaQuery和lambdaUpdate方法*/

        int remainBalance = user.getBalance() - money;
        //lambdaUpdate 方法用于创建 LambdaUpdateWrapper 实例，简化更新操作。
        lambdaUpdate()
                .set(User::getBalance,remainBalance)    //更新用户余额。
                .set(remainBalance == 0,User::getStatus,UserStatus.FROZEN)  //如果余额为 0，则更新用户状态为 2。
                .eq(User::getId,id)     //根据用户 ID 进行更新。
                .eq(User::getBalance,user.getBalance()) //乐观锁，比较用户余额是否等于刚查到的余额，保证并发安全性
                .update();
        /*乐观锁是一种并发控制机制，通过在更新时检查记录的版本号或特定字段的值，
        确保数据的一致性和并发安全性。在这段代码中，通过比较用户余额是否等于刚查到的余额来实现乐观锁。*/

    }




    @Override
    public List<User> queryUsers(String name, Integer status, Integer minBalance, Integer maxBalance) {

        /*IService中还提供了Lambda功能来简化我们的复杂查询及更新功能。
        Service中对LambdaQueryWrapper和LambdaUpdateWrapper的用法进一步做了简化。
        我们无需自己通过new的方式来创建Wrapper，而是直接调用lambdaQuery和lambdaUpdate方法*/

        //lambdaQuery 方法用于创建 LambdaQueryWrapper 实例，简化查询操作。
        return lambdaQuery()
                .like(name != null,User::getUsername,name)      //如果 name 不为空，按用户名进行模糊查询。
                .eq(status != null,User::getStatus,status)      //如果 status 不为空，按状态进行精确查询。
                .ge(minBalance != null,User::getBalance,minBalance) //如果 minBalance 不为空，查询余额大于等于 minBalance 的用户。
                .le(maxBalance != null,User::getBalance,maxBalance) //如果 maxBalance 不为空，查询余额小于等于 maxBalance 的用户。
                .list();


    }


    /*
    MyBatisPlus的静态工具类
    MyBatisPlus提供了一个名为Db的静态工具类，用于简化数据库操作。这个工具类的主要优点包括：
    无依赖注入：Db类是一个静态工具类，不需要通过依赖注入来获取实例，因此不会引入额外的依赖关系。
    简单易用：Db类提供了丰富的静态方法，可以直接在任何地方使用，无需关心依赖注入的问题。
    灵活性：可以在任何需要的地方使用Db类，而不必担心循环依赖的问题12。
    循环依赖
    循环依赖是指两个或多个Bean相互依赖，形成一个闭环。例如，ServiceA依赖于ServiceB，而ServiceB又依赖于ServiceA。这种情况会导致Spring容器在初始化Bean时无法正确解析依赖关系，从而抛出异常。
    解决循环依赖的问题
    使用MyBatisPlus的Db静态工具类可以有效避免循环依赖问题。因为Db类是静态的，不需要通过依赖注入来获取实例，因此在调用数据库操作时，不会引入额外的依赖关系。
    */

    /**
     * 静态工具
     * 有的时候Service之间也会相互调用，为了避免出现循环依赖问题，MybatisPlus提供一个静态工具类：Db，其中的一些静态方法与IService中方法签名基本一致，也可以帮助我们实现CRUD功能
     * @param id
     * @return
     */

    @Override
    public UserVO queryUserAndAddressById(Long id) {

        // 1.查询用户
        User user = getById(id);
        if (user == null || user.getStatus() == UserStatus.FROZEN){

            throw new RuntimeException("用户状态异常！");
        }

        // 2.查询地址
        List<Address> addresses = Db.lambdaQuery(Address.class).eq(Address::getUserId,id).list();
        //在查询地址时，我们采用了Db的静态方法，因此避免了注入AddressService，减少了循环依赖的风险。

        // 3.封装VO
        // 3.1转User的PO为VO
        UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);

        // 3.2转地址VO
        if (CollUtil.isNotEmpty(addresses)) {

            userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));
        }

        return userVO;
    }




    @Override
    public List<UserVO> queryUserAndAddressByIds(List<Long> ids) {

        // 1.查询用户
        List<User> users = listByIds(ids);
        // 使用listByIds方法根据传入的用户ID列表查询用户信息
        if (CollUtil.isEmpty(users)) {
            //用户为空

            return Collections.emptyList();
            // 如果查询结果为空，返回一个空集合
        }

        // 2.查询地址
        // 2.1.获取用户id集合
        // 虽然函数一开始已经传入了用户的ID列表，但在后续代码中仍然需要提取用户ID列表的原因主要有以下几点：
        //1. 确保数据一致性
        //传入的ids列表只是一个初始输入参数，实际查询到的用户列表users可能会有所不同。例如，某些ID可能在数据库中不存在，或者查询过程中可能会有其他过滤条件。因此，需要从实际查询到的用户列表中提取ID，以确保后续操作的数据一致性。
        //2. 过滤无效用户
        //在查询用户后，代码会检查用户列表是否为空。如果用户列表为空，直接返回空集合。这一步确保了后续操作只针对有效的用户进行。
        //3. 提取有效用户ID
        //通过users.stream().map(User::getId).collect(Collectors.toList())，从查询到的用户列表中提取有效的用户ID。这一步确保了后续查询地址时只使用有效的用户ID。
        //4. 避免重复查询
        //虽然传入了用户ID列表，但实际查询到的用户可能会有不同的ID。因此，需要从查询结果中提取ID，以避免重复查询和不必要的数据库操作。
        List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());
        // 将users列表转换为一个流。对流中的每个User对象调用getId方法，生成一个包含所有用户ID的新流。将这个新流中的元素收集到一个List<Long>中。即从查询到的用户列表中提取用户ID，生成一个新的ID列表。
        //users.stream():
        //功能：将users列表转换为一个流（Stream）。
        //原理：Stream是Java 8引入的一个新特性，用于处理集合数据。它提供了一种声明性的方法来处理数据集合。
        //流（Stream）：流是一个从支持数据处理操作的源生成的元素序列。流操作分为中间操作和终端操作，中间操作返回一个新的流，终端操作返回一个结果或副作用。
        //.map(User::getId):
        //功能：将流中的每个User对象映射为其ID。
        //原理：map方法用于将流中的每个元素应用一个函数，并将结果收集到一个新的流中。User::getId是一个方法引用，等价于user -> user.getId()，它表示对每个User对象调用getId方法。
        //方法引用（Method Reference）：方法引用是对现有方法的引用，使用::符号。它可以简化Lambda表达式的写法。
        //.collect(Collectors.toList()):
        //功能：将流中的元素收集到一个列表（List）中。
        //原理：collect方法用于将流中的元素收集到一个容器中。Collectors.toList()是一个收集器，它将流中的元素收集到一个List中。
        //收集器（Collector）：收集器是一个用于将流的元素累积到某种结果容器中的工具。Collectors.toList()是一个常用的收集器，用于将流的元素收集到一个列表中。

        // 2.2.根据用户id查询地址
        List<Address> addresses = Db.lambdaQuery(Address.class).in(Address::getUserId,userIds).list();
        //使用MyBatis Plus的静态工具类Db的静态方法lambdaQuery创建一个针对Address类的查询对象。添加一个查询条件，指定查询的用户ID在userIds列表中。执行查询并返回结果列表，包含所有符合条件的Address对象。
        //Db.lambdaQuery(Address.class):
        //功能：创建一个针对Address类的查询对象。
        //原理：Db是MyBatis Plus提供的一个静态工具类，lambdaQuery方法用于创建一个Lambda查询构造器。这个构造器可以用来构建查询条件。
        //.in(Address::getUserId, userIds):
        //功能：添加一个查询条件，指定查询的用户ID在userIds列表中。
        //原理：in方法用于构建SQL中的IN条件。Address::getUserId是一个方法引用，表示查询条件是Address表中的userId字段。userIds是一个包含用户ID的列表，表示查询条件中的值。
        //.list():
        //功能：执行查询并返回结果列表。
        //原理：list方法用于执行查询并将结果转换为一个列表。这个列表包含所有符合查询条件的Address对象。

        // 2.3.转换地址VO
        List<AddressVO> addressVOList = BeanUtil.copyToList(addresses, AddressVO.class);
        // 将查询到的Address对象列表转换为AddressVO对象列表
        //BeanUtil.copyToList(addresses, AddressVO.class):
        //功能：将addresses列表中的每个Address对象转换为AddressVO对象，并将这些AddressVO对象收集到一个新的列表中。
        //原理：BeanUtil是一个常用的工具类，用于简化Java Bean之间的属性复制。
        //copyToList方法：copyToList方法接受两个参数：一个是源对象列表，另一个是目标对象的类类型。它会遍历源对象列表，将每个源对象的属性复制到一个新的目标对象中，并将这些目标对象收集到一个新的列表中。

        // 2.4.用户地址集合分组处理，相同用户的放入一个集合（组）中
        Map<Long,List<AddressVO>> addressMap = new HashMap<>(0);
        //功能：创建一个空的HashMap，用于存储按用户ID分组的地址列表。
        //原理：HashMap是Java中的一个实现了Map接口的哈希表，Long类型的键表示用户ID，List<AddressVO>类型的值表示属于该用户的地址列表。
        //HashMap：HashMap是一个基于哈希表的实现，允许快速的键值对存取。这里用来存储按用户ID分组的地址列表。
        if(CollUtil.isNotEmpty(addressVOList)) {
            //功能：检查addressVOList是否为空。
            //原理：CollUtil.isNotEmpty是一个工具方法，用于检查集合是否为空。它比直接使用!addressVOList.isEmpty()更安全，因为它可以处理null值。

            addressMap = addressVOList.stream().collect(Collectors.groupingBy(AddressVO::getUserId));
            //功能：将地址列表按用户ID分组，并将结果存储在addressMap中。
            //原理：addressVOList.stream()：将addressVOList转换为一个流（Stream）。
            //     .collect(Collectors.groupingBy(AddressVO::getUserId))：使用Collectors.groupingBy收集器将流中的元素按用户ID分组。AddressVO::getUserId是一个方法引用，表示分组的依据是AddressVO对象的userId属性。
            //      Collectors.groupingBy：这是一个收集器，用于将流中的元素按某个属性分组。它返回一个Map，键是分组依据的属性值，值是属于该分组的元素列表。
        }


        // 3.转换VO返回
        List<UserVO> list = new ArrayList<>(users.size());
        //功能：创建一个UserVO对象列表，用于存储最终的结果。
        //原理：ArrayList是Java中的一个动态数组实现，users.size()指定了初始容量，确保列表有足够的空间存储所有用户视图对象。
        for (User user : users){

            // 3.1.转换User的PO为VO
            UserVO vo = BeanUtil.copyProperties(user,UserVO.class);
            // 将User对象转换为UserVO对象
            list.add(vo);
            //将转换后的UserVO对象添加到结果列表中。

            // 3.2.转换地址为VO
            vo.setAddresses(addressMap.get(user.getId()));
            //功能：将分组后的地址列表设置到对应的UserVO对象中。
            //原理：addressMap是一个按用户ID分组的地址列表。get(user.getId())方法返回与当前用户ID对应的地址列表，并将其设置到UserVO对象的addresses属性中。

        }

        return list;
        //功能：返回最终的UserVO对象列表。
        //原理：方法返回值是一个包含所有用户视图对象的列表。

    }





}
